Apk安装之谜
戳蓝字“牛晓伟”关注我哦!
用心坚持输出易读、有趣、有深度、高质量、体系化的技术文章
本文概要
本文同样还是延续自述、对话这种轻松的方式,带您揭开apk安装的谜底,apk它到底是如何安装的。(为什么没有继续写系统native进程的文章,而来写apk安装的文章,主要原因是在看installd代码的时候也结合了apk安装的代码,如果放到后面写apk安装的文章,怕时间久了很多就忘记了,因此才有了此篇文章。文中的代码是基于android13)
Android系统进程系列的前五篇文章如下:
Android系统native进程之我是init进程
Android系统native进程之属性能力的设计魅力
Android系统native进程之进程杀手--lmkd
Android系统native进程之日志系统--logd、logcat
Android系统native进程之我是installd进程
开场白
“大家好,我是今天的主角apk,今天的给大家带来的主题是apk安装之谜,我请到了PackageManagerService、Settings、PackageInstallerSession、PackageInstallerService、InstallPackageHelper、Installer作为嘉宾,那就让嘉宾先做下自我介绍吧。”
PackageManagerService:“大家对我一定非常的了解了,我是一个服务管理所有的已经安装的apk,运行于systemserver进程,可以称呼我PMS哦。”
Settings:“大家好啊,看到我的名字有可能会有人认为我是为设置app服务的,其实不然,我是为apk的安装服务的,哪些apk安装了、安装的时间信息等等我都保存着,并且会持久化到内部存储空间。”
PackageInstallerService:“大家好,我和PMS一样也是运行于systemserver进程的,一看我的名字就能知道我是和apk安装有关系的,如果谁有安装apk的需求,可以直接通过binder的方式'呼我哦',可以称呼我PIS哦。”
PackageInstallerSession:“大家好,我的名字和PIS是不是很像啊,可别听PIS忽悠啊,真正进行apk安装的工作都是由我完成的,可以称呼我为Session哦。”
InstallPackageHelper:“大家好,我的主要工作是负责apk安装中期的工作,后面到了我的工作内容的时候,会着重在介绍。”
Installer:“大家好,我是installd进程的代理,在java世界如果需要使用installd的能力的话,直接调用我即可,我会把相关的请求‘转告’给installd,我在apk的安装中也起了很大的作用。”
既然嘉宾都介绍完自己了,因为今天我是主角,我有主角光环,那我非常有必要隆重、浓墨重彩的介绍下我自己,让大家对我有一个非常深刻的了解。
我是apk
apk它是 Android Package 的缩写。我是一个zip格式的压缩文件,只不过为了能让大家从文件名上一眼认出我来,我的文件后缀是 .apk。
一个apk内主要包含了dex文件
、so文件
、res目录
、resources.arsc
、META-INF目录
(它里面的CERT.SF
、CERT.RSA
文件主要是和签名证书有关的) 、AndroidManifest.xml
等文件和目录,对于dex等文件大家肯定都熟悉了,我就不在这赘述了,我着重来介绍下AndroidManifest.xml(Android清单文件)。
估计会有人说AndroidManifest文件有啥好说的,我们都知道它,它会把定义了的四大组件及Application声明在内,同时声明需要用到的权限、meta-data等信息。这么简单的大家都熟知的知识就不用介绍了。但是我想要介绍的是为啥要有AndroidManifest以及它是被谁使用的?
拿你们人类去餐馆吃饭作对比,去餐馆吃饭的时候会有菜单,菜单的作用就是告诉顾客我餐馆都提供哪些菜品,对于这些菜是由哪个厨师加了哪些配料做成的,顾客都不需要关心。而AndroidManifest的作用如菜单一样,它会把自己apk声明的信息展示出来,餐馆的菜单是展示给顾客的,而AndroidManifest是展示给PMS,也就是说PMS相关的解析代码会从apk的AndroidManifest中解析出所有的信息,从而知道apk声明了哪些四大组件、apk的包名是啥、声明了哪些权限等信息。AndroidManifest的作用就是告诉PMS:如果想了解我都声明了哪些四大组件,以及声明了哪些权限、meta-data等,直接读我都可以知道。
我的“归宿”
我的“归宿”就是成功的安装到各种安卓设备上某个目录,只有在那里我才能在这台设备上充分的发挥我的价值。
像人类一样有三六九等之分,而apk也是存在不同类别划分的,大致划分为系统apk(如launcher、dialer、setting等)和普通apk(如微信、抖音)。当然系统apk还可以进一步的划分为核心系统apk、厂商apk等。不同类型的apk,它们所在的父目录也是不同的,系统apk它们的父目录是 /system/priv-app、/system/app 等,而普通apk它们的父目录是 /data/app。如下:
drwxr-xr-x 39 root root 4096 2023-08-20 21:26 system/priv-app
drwxr-xr-x 24 root root 4096 2023-08-20 21:25 system/app
/system/priv-app和/system/app目录,它们的user和group都是 root,也就是只有root用户可以对这俩目录进行读写执行操作,其他用户只有执行权限。关于/data/app目录的详细信息会在下面介绍。
你们人类有句谚语:条条大路通罗马,而有的人却生在罗马。系统apk,它们就是出生在“罗马”,一“出生“就有“归宿”。而作为普通apk的我,却是在通往“罗马”的路上,因为我一“出生“会被放置于服务器上或者电脑上的某个黑暗的目录,如果想要到达我的“罗马”就需要通过apk安装把我安装到设备上。
关于我的介绍就到此吧,进入咱们今天的正题吧。
安装apk这件事
PMI:“因为我制定了安装apk相关的大体架构和流程上的事情,因此就由我来介绍下安装apk这件事情吧,关于具体实现细节还需要Session在后面介绍。”
apk的安装方式有 adb install命令、应用商店、安装apk的程序。它们的区别首先在于apk的来源不同:adb install的apk来源于电脑、应用商店的apk是应用商店从服务器下载成功后进行安装、通过安装apk的程序的apk来源于设备上已经存在的apk;其次是是否提供友好的交互界面。不管是哪种方式它们的安装流程基本上是一致的。
我把安装apk总结为三阶段:前期准备、安装、后期收尾。
前期准备的工作有拷贝、完整性校验、解析apk、提取native libs、版本号验证;安装的工作有准备 (Prepare) 、扫描 (Scan) 、调和 (Reconcile) 、提交 (Commit) ; 后期收尾的工作有创建app data根目录、dex优化、移除已有apk、发送安装成功广播。
那我们就按上面的三阶段来给大家揭开apk安装的谜底吧。
前期准备
PIS:“谁要想安装apk,首先需要调用我的PackageInstallerService的createSession方法创建一个PackageInstallerSession,一次安装会对应唯一的一个PackageInstallerSession,PackageInstallerSession才是真正干活的主力,后面的安装环节就由PackageInstallerSession来给大家介绍了,PackageInstallerSession简称Session。”
Session:“我会给使用者一个sessionId,通过这个sessionId可以从PIS找到我,拷贝是安装apk的第一步,那我们就从拷贝开始。”
拷贝
Session:“安装apk第一步是需要把apk(不管apk来源于哪)进行拷贝,拷贝到 /data/app/xxxx.tmp(xxxx是一个随机的字符串)目录下面,拷贝的apk的名字一般被命名为:base.apk,拷贝完后的apk文件的路径是 /data/app/xxxx.tmp/base.apk 这样的。”
apk:“一上来就要拷贝,这一下子把我搞懵逼了,能说说为啥要拷贝吗?我的理解是拷贝会增加apk的安装时长,如果apk特别大,安装时长更会加长,不拷贝不行吗?”
Session:“不拷贝还真不行,那我就来说下原因。“
拿adb install或者应用市场安装apk的方式来说明问题吧,Session我是运行于systemserver进程。通过adb install安装的话,apk是位于pc上,pc上的apk对于Session是肯定不能拿来直接用的;通过应用市场安装的话,apk是被应用市场进程所存储的,而Session我也是基本不可以访问的(除非apk被下载到可共享的目录)。因此我需要先把apk拷贝到我可以访问的目录下面,这样我就可以直接操作apk了。
apk:“我同意你的说法,对于adb install安装确实需要拷贝,因为apk是存储于pc上。但是通过应用市场安装是不是可以这样做:就是应用市场在从服务器下载apk的时候直接下载到一个约定好的目录中,Session你可以直接从这个目录来操作apk了,这样就不需要拷贝的过程了,安装速度肯定可以提升。”
Session:“对于你的提议是存在几个问题,约定好的目录这个目录应该是一个共享目录吧,第一个问题是:怎么样做到只有我和应用市场进程才能访问这个目录?第二个问题是:即使可以做到共享还需要对该目录进行保护,在安装期间应用市场进程是不可以对该目录进行任何修改的,也就是在安装期间只有Session我才可以操作这个共享目录。解决上面的两个问题是不是比较麻烦啊。”
apk:“说的极是,我确实没想到你说的这些问题。”
还有非常重要的一点,拷贝到的 /data/app/xxxx.tmp 目录,这个目录有一个非常重要的特性,这个目录的user和group都是system,也就是只有systemserver进程对此目录具有读写执行权限,而其他进程只有读权限,这样就可以保证被拷贝apk的安全性了,只有Session我才可以访问、修改该目录。(如下图)
对apk拷贝可以调用我的write方法,调用我所有的方法都需要进行binder跨进程调用。
该步的产物是 /data/app/xxxx.tmp/base.apk ,后面的安装流程都基于此apk进行。 关于拷贝就介绍到此,咱们接着介绍完整性验证。
完整性验证
Session:“第二步是对 /data/app/xxxx.tmp/base.apk 进行完整性验证。完整性验证 用一句话概括就是:验证apk有没有被改过。这一步肯定是要最先进行的,只有我先确认apk是一个完整的apk才有必要进行后面的安装流程。“
apk问到:“apk被改动会存在哪些危害?”
Session:“比如有个高人下载了微信的apk,抛开加固等黑科技,这位高人解压了微信apk,并且在其中插入了自己的代码(比如把聊天信息上传到自己的服务器上)在重新打包成微信apk,那这个时候的微信apk被用户安装上的话,你可以想想这有多危险,用户和别人的聊天信息他都可以知道了。apk完整性验证就是要验证apk有没有被改过,改过的话那就完全认为这个apk是被动过手脚的,肯定不允许安装的。”
apk:“那又是如何能验证apk没有被改动过呢?”
Session:"我先从雏形说起,这样可以更容易理解从雏形到最终方案是如何一步一步形成的。刚开始的验证雏形是这样的:我Session需要从apk内拿到一个信息,这个信息是与apk是一一对应关系,也就是apk内不管发生任何变化,那这个信息也需要发生变化,并且我需要根据apk能推导或计算出这个信息,如果推导或计算的信息与apk内拿到的信息一致就可以证明apk是没有被修改过的。那怎么样可以做到呢?答案是使用摘要算法。“
apk迷惑的问到:“摘要算法,这又是啥子嘛?”
举个例子人类读完一篇文章后,这篇文章总会有个中心思想之类的总结,那这个总结就是一个摘要。
摘要算法:会接受一个输入,不论输入的内容是多长都会输出一个固定长度的内容,输入内容一样才会有一样的输出,输入内容不一样输出内容也不会一样,并且这个输出内容是不可逆的。
可以使用摘要算法对apk的各种文件生成摘要,这些摘要信息会写入apk。验证apk完整性的进一步思路是这样的:使用摘要算法对apk的各种文件生成摘要,如果生成的摘要与apk内存的摘要信息一致则证明apk是没有被修改过的。
Session:"apk老兄,你觉得上面的的思路有啥问题吗?“
apk:“我想想啊,想到了,这些摘要信息没有加密,如若改动了apk内的内容,则也可以重新把摘要内容改了,重新打包到apk内。因此需要对摘要信息进行加密。”
你说的非常的对,对摘要信息进行加密需要用到非对称加密(https中就用到它),非对称加密是一种加密算法,分为公钥和私钥,公钥是可以公开的,私钥是不能公开的,用私钥对信息加密,是可以用公钥解密的。需要用私钥对摘要信息进行加密,把加密后的摘要信息和证书(证书存储了公钥和开发者的一些信息)一同打包到apk内。我把这个过程起了一个很好听的名字apk签名,就如人类在合同上签名一样,每个apk也是需要签名的,签了名后这个apk就和开发者绑定了。
总结下apk签名的过程:首先用摘要算法对apk内的各种文件生成摘要;其次使用非对称加密的私钥对这些摘要信息加密;最后把加密的摘要信息和证书(公钥和开发者信息)写入apk内。这只是对apk v1签名算法的一个简单总结,签名算法有v1、v2、v3、v4四个版本,每个版本都是为了解决前者存在的问题而诞生的。
基于apk签名,终极apk完整性验证流程如下(下面主要介绍的是签名v1版本的验证流程):
从apk中拿到证书信息,拿到加密的摘要信息 从证书中用公钥对加密的摘要信息解密,解密出摘要信息 对apk的各文件用摘要算法生成摘要,并与解密出的摘要信息进行对比,如若一致则证明没有被改动,否则发生了改动。
除了验证apk的完整性外,还会从apk中的提取签名信息,签名信息保存在SigningDetails对象中,在后面的安装流程中是要用到SigningDetails信息的,如果apk没有获取到签名信息,则会停止安装(正常咱们开发的debug版的app是已经默认进行了签名)。
apk完整性验证是安装的必要环节,如果apk完整性验证失败,则停止安装;否则继续下一步的安装流程。该步的产物是SigningDetails对象以及apk是否完整的,SigningDetails对象会在后面的安装流程用到。
完整性验证的部分代码如下,有兴趣可以看下:
//文件路径:frameworks/base/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java
//获取签名信息,签名信息存储在SigningDetails
public static ParseResult<SigningDetails> getSigningDetails(ParseInput input,
String baseCodePath, boolean skipVerify, boolean isStaticSharedLibrary,
@NonNull SigningDetails existingSigningDetails, int targetSdk) {
省略代码......
//跳过验证,走这
if (skipVerify) {
省略代码......
} else {
//验证并且返回签名信息,会对apk的完整性进行校验,并且返回签名信息
verified = ApkSignatureVerifier.verify(input, baseCodePath, minSignatureScheme);
}
......省略代码
}
解析apk
Session:“有没有发现,前两步我对于安装的apk是知之甚少的,我不知道安装apk的包名、它的名字、版本号等基础信息。但是这些信息是非常非常重要的,因此这一步需要把这些信息解析出来,为后面的安装流程做准备。我把这一步称为解析apk,解析apk说的更具体点就是解析apk中的AndroidManifest(清单文件),从AndroidManifest文件中把包名、版本号、安装路径、是否是debug、是否是多架构、是否提取native libs等信息提取出来放入PackageLite对象。并不会提取四大组件信息、权限等信息,因为还暂时用不到这些信息,多解析这些信息就需要多花时间,我秉持一个用时才去解析的原则。”
该步的产物是PackageLite对象,该步的产物会在后面的安装流程用到,下一步就需要做提取native libs的操作。
PackageLite类的关键属性如下,有兴趣可以看下
//文件路径: frameworks/base/core/java/android/content/pm/parsing/PackageLite.java
public class PackageLite {
//包名
private final @NonNull String mPackageName;
//base apk的路径
private final @NonNull String mBaseApkPath;
//版本号
private final int mVersionCode;
//app是否是debug版本
private final boolean mDebuggable;
//是否是多架构(32位和64位)
private final boolean mMultiArch;
//是否提取native libs
private final boolean mExtractNativeLibs;
......省略其他属性
}
提取native libs
Session:“这一步所要做的事情是提取native libs(native libs指的是apk中的so库),提取native libs:也就是把apk中的so库提取到 /data/app/xxxx.tmp/lib/cpuabi/ (cpuabi是当前设备的cpu架构比如arm、arm64)目录下面。abi是Application Binary Interface的缩写,应用程序二进制接口。但是并不是所有的apk都包含了so,如果没有包含则不会执行此步。提取native libs会用到解析apk这一步解析出的PackageLite信息”
apk中so库的所处的目录如下:
//base.apk,该apk中包含了两个abi:arm、arm64,(为了减小apk的大小,现在的apk都只保留一个abi)
lib/arm/xx.so
lib/arm/xxx.so
lib/arm/xxxx.so
lib/arm64/xx.so
lib/arm64/xxx.so
lib/arm64/xxxx.so
在提取native libs的时候,会检测apk中的cpu abi是否与当前设备的cpu abi是否匹配,如果不匹配比如当前设备cpu abi是x86_64的,而apk中的cpu abi只有arm、arm64,那这种情况肯定是不能继续安装的,因为so库是与cpu abi强相关的,arm下面的so库在x86_64上面运行肯定是出问题的。为了考虑性能和方便性,整个提取native libs都是委托给native的代码执行的。
提前native libs可以提前检测当前设备的cpu abi是否与apk中的so库匹配,不匹配则不安装,并且还可以提升app的启动速度,如果不提取的话,每次app启动都需要从apk中解析出这些so库,这速度肯定慢啊,该步的产物是apk中的so库提取到 /data/app/xxxx.tmp/lib/cpuabi/ 目录下面。下一步就来看下版本号验证吧。
对应的代码如下,有兴趣可以看下:
//文件路径:frameworks/base/services/core/java/com/android/server/pm/PackageInstallerSession.java
private void parseApkAndExtractNativeLibraries() throws PackageManagerException {
synchronized (mLock) {
省略代码......
final PackageLite result;
if (!isApexSession()) {
//走这,解析apk信息
result = getOrParsePackageLiteLocked(stageDir, /* flags */ 0);
} else {
result = getOrParsePackageLiteLocked(mResolvedBaseFile, /* flags */ 0);
}
if (result != null) {
mPackageLite = result;
if (!isApexSession()) {
省略代码......
//提取so库
extractNativeLibraries(
mPackageLite, stageDir, params.abiOverride, mayInheritNativeLibs());
}
}
}
}
版本号验证
Session:“到了版本号验证这一步了,但是这步不是必须,如果设备上已经安装了相同包名的apk,则该步是必须的,版本号验证所要做的事情非常简单:正在安装的apk的版本号是否比已经安装的apk的版本号小,小的话就停止安装。正在安装的apk的版本号从解析apk中的PackageLite拿到。”
对应的代码如下,有兴趣可以看下:
//文件路径:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
Pair<Integer, String> verifyReplacingVersionCode(PackageInfoLite pkgLite,
long requiredInstalledVersionCode, int installFlags) {
if ((installFlags & PackageManager.INSTALL_APEX) != 0) {
return verifyReplacingVersionCodeForApex(
pkgLite, requiredInstalledVersionCode, installFlags);
}
String packageName = pkgLite.packageName;
synchronized (mPm.mLock) {
省略代码......
//dataOwnerPkg代表设备已经安装对应的apk了
if (dataOwnerPkg != null && !dataOwnerPkg.isSdkLibrary()) {
//只有debug版本才允许版本降级
if (!PackageManagerServiceUtils.isDowngradePermitted(installFlags,
dataOwnerPkg.isDebuggable())) {
try {
//检测是否存在版本降级,是的话会报错
PackageManagerServiceUtils.checkDowngrade(dataOwnerPkg, pkgLite);
} catch (PackageManagerException e) {
String errorMsg = "Downgrade detected: " + e.getMessage();
Slog.w(TAG, errorMsg);
return Pair.create(
PackageManager.INSTALL_FAILED_VERSION_DOWNGRADE, errorMsg);
}
}
}
}
return Pair.create(PackageManager.INSTALL_SUCCEEDED, null);
}
总结
前期准备阶段又划分为拷贝、完整性验证、解析apk、提取native libs、版本号验证五步,每一步都在为后一步做准备。
拷贝会把安装的apk拷贝到/data/app/xxxx.tmp/base.apk。
完整性验证会对/data/app/xxxx.tmp/base.apk进行验证,如果修改过则停止安装,同时还会提取签名信息到SigningDetails对象,如果apk没有签名信息则会停止安装,SigningDetails对象会在后面的安装流程用到。
解析apk会从/data/app/xxxx.tmp/base.apk的AndroidManifest中把包名、版本号、安装路径、是否是debug等信息提取出来放入PackageLite对象,若解析中发生错误也会停止安装。
提取native libs的时候会用到 PackageLite对象,会把/data/app/xxxx.tmp/base.apk中的so库提取到 /data/app/xxxx.tmp/lib/cpuabi/ 目录(若apk存在so库),若发生错误则也会停止安装。
版本号验证的工作内容是正在安装的apk的版本号是否比已经安装的apk的版本号小,小的话就停止安装。这步不是必须的,只有设备上已经安装了相同包名的apk才执行。
前期准备的各步都能正常执行的话,就进入正式的安装阶段,那我们来看下安装阶段的内容。
安装
Session:“具体安装阶段的工作内容由InstallPackageHelper来完成的,那就有请它来给大家介绍。”
InstallPackageHelper:“大家好啊,终于轮到我出场了,安装阶段也可以称为正式安装,在这阶段才真正开始apk的安装工作。那我就来介绍下吧。”
安装阶段可以分为四步:准备 (Prepare) 、扫描 (Scan)、调和 (Reconcile)、提交 (Commit),这四步整体是原子化操作,也就是只要有一个出问题,整体的安装就停止,下面就来介绍下这四步。
准备 (Prepare)
完全解析apk
还记得解析apk那步会把apk的基础信息存放到PackageLite对象吗,这只是解析了比较少的基础信息。完全解析apk就是从/data/app/xxxx.tmp/base.apk的AndroidManifest中把所有的信息都解析出来,包含了声明的四大组件、权限、meta-data、shareLibs等,这些信息会存放在ParsedPackage对象中,如果解析发生错误,则停止安装。解析出ParsedPackage后,后面的工作都是围绕ParsedPackage展开的。
保存签名
在完整性验证那步是保存了签名信息到SigningDetails对象的,如果SigningDetails不为null的话会把SigningDetails存入ParsedPackage中;否则从apk中解析出SigningDetails存入ParsedPackage。
签名验证
签名验证的工作内容是对正在安装的apk的证书信息与设备上已经安装的相同包名的apk的证书信息进行对比,如果不一致,则停止安装。如果设备上不存在相同包名的apk则这一步是不会进行的。比如设备上安装了微信,如果有一个apk它的包名与微信一样,签名肯定不一样的情况下。这时候往设备上安装此apk肯定是安装不上的。
权限验证
权限验证就是根据ParsedPackage里的getPermissions()方法获取的权限,来判断哪些权限是存在问题的,比如声明了只有系统app才能使用的权限,如果存在问题则停止安装。
重命名
还记得拷贝第一步的时候生成的临时目录 /data/app/xxxx.tmp/ 吗?这毕竟是个临时目录,是有必要给它一个正式的名字的,那重命名所做的事情就是把 /data/app/xxxx.tmp/ 重命名为 /data/app/~~[randomStrA]/[packageName]-[randomStrB](其中randomStrA、randomStrB是随机生成的字符串,packageName是包名),这个名字看上去确实不是很正规,但是它确实是一个非常正式的名字。
apk:“那我有个问题啊,为什么重命名的名字没有用包名,而是用一个随机字符串呢?”
InstallPackageHelper:“用随机字符串的原因是,在 /data/app/ 目录下面会存在两个同一包名apk的情况,如果用包名的话会出现问题。比如当前设备上已经安装了一个微信apk,则在 /data/app/com.weixin/ 目录下会存在微信的apk。这时候安装一个高版本的微信apk的,这时候重命名的话就出现问题,因为已经有com.weixin目录存在了。”
如果重命名失败也会停止安装。下面是重命名的例子,可以看到它们的user、group都是system
如下正式apk父目录的相关代码,有兴趣可以看下
//文件路径:services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
/**
* 返回的目录结构样子: targetDir/~~[randomStrA]/[packageName]-[randomStrB]
*/
public static File getNextCodePath(File targetDir, String packageName) {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[16];
File firstLevelDir;
do {
random.nextBytes(bytes);
String firstLevelDirName = RANDOM_DIR_PREFIX
+ Base64.encodeToString(bytes, Base64.URL_SAFE | Base64.NO_WRAP);
firstLevelDir = new File(targetDir, firstLevelDirName);
} while (firstLevelDir.exists());
random.nextBytes(bytes);
String dirName = packageName + RANDOM_CODEPATH_PREFIX + Base64.encodeToString(bytes,
Base64.URL_SAFE | Base64.NO_WRAP);
final File result = new File(firstLevelDir, dirName);
if (DEBUG && !Objects.equals(tryParsePackageName(result.getName()), packageName)) {
throw new RuntimeException(
"codepath is off: " + result.getName() + " (" + packageName + ")");
}
return result;
}
总结
当然除了上面的这些工作外,还做了尝试杀死当前同包名的app进程(如果设备上已经有相同包名的apk并且处于运行状态),构造需要移除的信息PackageRemovedInfo对象(如果设备上已经有相同包名的apk,则需要把它的信息在后面的流程中移除掉,因为这些信息毕竟是老apk的信息)。
准备阶段所做的主要事情有:把/data/app/xxxx.tmp/base.apk的AndroidManifest中把所有的信息都解析出来,存在ParsedPackage对象中,进行了签名、权限等验证,把/data/app/xxxx.tmp/目录重命名为 /data/app/~~[randomStrA]/[packageName]-[randomStrB] 目录。若准备阶段发生了错误,则会停止安装。准备阶段的产物是ParsedPackage(它在后面的安装流程会用到),咱们进入扫描阶段。
准备阶段对应的一部分源码如下(源码实在是太多了,只列出一部分),有兴趣可以看下
private PrepareResult preparePackageLI(InstallArgs args, PackageInstalledInfo res)
throws PrepareFailure {
省略代码......
final ParsedPackage parsedPackage;
try (PackageParser2 pp = mPm.mInjector.getPreparingPackageParser()) {
//完全解析apk
parsedPackage = pp.parsePackage(tmpPackageFile, parseFlags, false); //niu 解析apk中更具体的信息 放入ParsedPackage
AndroidPackageUtils.validatePackageDexMetadata(parsedPackage);
} catch (PackageManagerException e) {
throw new PrepareFailure("Failed parse during installPackageLI", e);
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
省略代码......
//设置签名信息
if (args.mSigningDetails != SigningDetails.UNKNOWN) {
parsedPackage.setSigningDetails(args.mSigningDetails);
} else {
final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing();
final ParseResult<SigningDetails> result = ParsingPackageUtils.getSigningDetails(
input, parsedPackage, false /*skipVerify*/);
if (result.isError()) {
throw new PrepareFailure("Failed collect during installPackageLI",
result.getException());
}
parsedPackage.setSigningDetails(result.getResult());
}
省略代码......
}
扫描 (Scan)
InstallPackageHelper:“扫描这步主要的作用就是完善ParsedPackage的信息,同时用ParsedPackage的信息创建或者更新已有的PackageSetting。关于PackageSetting还需要有请Settings来介绍下。”
Settings:“在安装apk后,肯定需要把安装的apk相关的信息记录下来,这些信息比如包名、版本号、apk路径、native code路径、appid、签名相关信息等,这些信息都是与安装的apk是一一对应并且不会变化的。而还有一些信息是与当前设备的用户有关的(比如当前设备存在多用户),则是需要记录下每个用户是否安装了这个apk、安装apk的时间等信息。 上面的这些信息肯定是需要记录并且需要持久化到内部存储空间的。这些信息被放在PackageSetting对象中,一个已安装的apk会对应自己的PackageSetting,也就是说PackageSetting存储了已安装apk相关的信息。而这些信息会最终持久化到packages.xml文件中。”
生成appId
每个被安装的apk都会有自己的appId,appId它是一个整数,如果在AndroidManifest中配置了android:sharedUserId则配置了相同sharedUserId的apk的appId是一样的。扫描的最后一步是为apk生成它的appId,这样被安装的apk就有了“正式身份”。
生成appId的代码如下,有兴趣看下:
//文件路径:Settings.java
boolean registerAppIdLPw(PackageSetting p, boolean forceNew) throws PackageManagerException {
final boolean createdNew;
Slog.i(TAG, "niulog install registerAppIdLPw p:" + p + " forceNew:" + forceNew + " appid:" + p.getAppId());
if (p.getAppId() == 0 || forceNew) {
// Assign new user ID
p.setAppId(mAppIds.acquireAndRegisterNewAppId(p));
createdNew = true;
} else {
// Add new setting to list of user IDs
createdNew = mAppIds.registerExistingAppId(p.getAppId(), p, p.getPackageName());
}
if (p.getAppId() < 0) {
PackageManagerService.reportSettingsProblem(Log.WARN,
"Package " + p.getPackageName() + " could not be assigned a valid UID");
throw new PackageManagerException(INSTALL_FAILED_INSUFFICIENT_STORAGE,
"Package " + p.getPackageName() + " could not be assigned a valid UID");
}
return createdNew;
}
扫描这步会为apk生成appId,同时会完善ParsedPackage的信息,扫描过程如果发生错误也会停止安装。这一步的产物是PackageSetting,它会被后面的安装流程用到。下面来介绍下调和这步。
调和 (Reconcile)
调和这步主要是利用准备、扫描的产物来验证当前apk使用到的shared libs是否存在、真实有效、是否重复申请等,如果验证失败则停止安装,比如在apk的AndroidManifest文件中用
下面是它的源码(由于源码篇幅太长,只把方法名展示出来),有兴趣可以看下
//文件名:ReconcilePackageUtils.java
public static Map<String, ReconciledPackage> reconcilePackages(
final ReconcileRequest request, SharedLibrariesImpl sharedLibraries,
KeySetManagerService ksms, Settings settings)
throws ReconcileFailure {
省略代码......
}
提交 (Commit)
InstallPackageHelper:“提交是安装的最后一步了,提交的主要工作内容就是对上面准备、扫描、调和的产物PackageSetting和ParsedPackage提交给Settings和PMS,让它们把各自更新自己的状态。那就由它们来介绍吧。”
Settings:“首先由我来介绍吧,调用我的insertPackageSettingLPw方法可以把PackageSetting和ParsedPackage更新我的mPackages属性 (它以包名为key,PackageSetting为value存放所有的已安装apk)。并且会把它们持久化到packages.xml文件,这样当下次设备重新启动的时候,就可以从packages.xml中把所有已安装apk的信息都读取到,每个已安装apk对应自己的PackageSetting,如果想知道当前设备已安装了哪些apk,可以‘呼我哦’。”
PMS:“该轮到我了,我有个非常重要的属性mPackages (它同样以包名为key,AndroidPackage为value存放所有已安装的apk,ParsedPackage是AndroidPackage的子类) ,我会把ParsedPackage添加到mPackages属性中。同时我还有个属性mComponentResolver,它可以把ParsedPackage中的四大组件‘收拢’起来。只有经过这些操作,在运行该apk的时候才能从我这检索到对应apk里面的四大组件信息,进而apk才能运行。”
总结
InstallPackageHelper郑重的对apk说:“恭喜你啊,经过安装阶段,你终于找到了你的‘归宿’ /data/app/~~[randomStrA]/[packageName]-[randomStrB] 目录,从此你就可以在这台设备上发挥你的价值了。这个目录它的user和group都是system,也就是说只有systemserver进程才有权读写执行该目录,而其他用户只能读的权限,这样就可以保证该目录的安全性。这也就是为啥在apk运行时候,是可以把该目录下的apk文件和lib下的各种so文件加载到自己进程的ClassLoader的原因。”
InstallPackageHelper又说:“虽然apk你找到了自己的‘归宿’,但是你的AndroidManifest声明的各种数据还没有传递给PMS,因为PMS是包管理者它管理着系统里的所有的apk信息,系统中谁想知道哪个apk安装了?哪个apk都声明了哪些组件?哪个apk声明了哪些权限等等这些信息都需要向PMS来要。因此需要把从apk中的AndroidManifest中解析出来的ParsedPackage信息传递给PMS,这样其他查询者比如ActivityManagerService就可以从PMS查到这些信息了。”
InstallPackageHelper:“系统里面安装了哪些apk,这都是需要记录并且持久化到内部存储空间的,而Settings就负责这个事件,新安装的apk会生成一个PackageSetting对象(它记录了apk的包名、版本号、签名信息、apk路径、哪些user安装了、安装时间等信息),PackageSetting对象会传递给Settings,Settings把它加入内存并且持久化到packages.xml文件中。”
下面是安装阶段的代码,有兴趣看下
//文件:InstallPackageHelper.java
private void installPackagesLI(List<InstallRequest> requests) {
final Map<String, ScanResult> preparedScans = new ArrayMap<>(requests.size());
final Map<String, InstallArgs> installArgs = new ArrayMap<>(requests.size());
final Map<String, PackageInstalledInfo> installResults = new ArrayMap<>(requests.size());
final Map<String, PrepareResult> prepareResults = new ArrayMap<>(requests.size());
final Map<String, Settings.VersionInfo> versionInfos = new ArrayMap<>(requests.size());
final Map<String, Boolean> createdAppId = new ArrayMap<>(requests.size());
boolean success = false;
try {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "installPackagesLI");
for (InstallRequest request : requests) {
// TODO(b/109941548): remove this once we've pulled everything from it and into
// scan, reconcile or commit.
final PrepareResult prepareResult;
try {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "preparePackage");
//1.prepare阶段,会解析apk中的信息主要是AndroidManifest,解析出来的实体是ParsedPackage(解析的信息更全包含了四大组件等),若不是一个正确的apk则不会继续下面的步骤;若是正确的apk,则会对apk的签名、shareuserid以及是替换老apk还是新apk做处理
prepareResult =
preparePackageLI(request.mArgs, request.mInstallResult);
} catch (PrepareFailure prepareFailure) {
request.mInstallResult.setError(prepareFailure.error,
prepareFailure.getMessage());
request.mInstallResult.mOrigPackage = prepareFailure.mConflictingPackage;
request.mInstallResult.mOrigPermission = prepareFailure.mConflictingPermission;
return;
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
request.mInstallResult.setReturnCode(PackageManager.INSTALL_SUCCEEDED);
request.mInstallResult.mInstallerPackageName =
request.mArgs.mInstallSource.installerPackageName;
final String packageName = prepareResult.mPackageToScan.getPackageName();
Slog.i(TAG,"niulog install installPackagesLI prepare request = "+request+" packageName = "+packageName);
prepareResults.put(packageName, prepareResult);
installResults.put(packageName, request.mInstallResult);
installArgs.put(packageName, request.mArgs);
try {
// 2.扫描阶段,扫描阶段主要是构造或者使用原有的PkgSetting
final ScanResult result = scanPackageTracedLI(
prepareResult.mPackageToScan, prepareResult.mParseFlags,
prepareResult.mScanFlags, System.currentTimeMillis(),
request.mArgs.mUser, request.mArgs.mAbiOverride);
if (null != preparedScans.put(result.mPkgSetting.getPkg().getPackageName(),
result)) {
request.mInstallResult.setError(
PackageManager.INSTALL_FAILED_DUPLICATE_PACKAGE,
"Duplicate package "
+ result.mPkgSetting.getPkg().getPackageName()
+ " in multi-package install request.");
return;
}
if (!checkNoAppStorageIsConsistent(
result.mRequest.mOldPkg, result.mPkgSetting.getPkg())) {
// TODO: INSTALL_FAILED_UPDATE_INCOMPATIBLE is about incomptabible
// signatures. Is there a better error code?
request.mInstallResult.setError(
INSTALL_FAILED_UPDATE_INCOMPATIBLE,
"Update attempted to change value of "
+ PackageManager.PROPERTY_NO_APP_DATA_STORAGE);
return;
}
createdAppId.put(packageName, optimisticallyRegisterAppId(result)); //niu 生成或者使用原有appid
versionInfos.put(result.mPkgSetting.getPkg().getPackageName(),
mPm.getSettingsVersionForPackage(result.mPkgSetting.getPkg()));
Slog.i(TAG,"niulog install installPackagesLI scan request = "+request+" ScanResult.result = "+result+" versionInfo:"+mPm.getSettingsVersionForPackage(result.mPkgSetting.getPkg()));
} catch (PackageManagerException e) {
request.mInstallResult.setError("Scanning Failed.", e);
return;
}
}
ReconcileRequest reconcileRequest = new ReconcileRequest(preparedScans, installArgs,
installResults, prepareResults,
Collections.unmodifiableMap(mPm.mPackages), versionInfos); //niu 用prepare和scan阶段的数据构造ReconcileRequest
CommitRequest commitRequest = null;
synchronized (mPm.mLock) {
Map<String, ReconciledPackage> reconciledPackages;
try {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "reconcilePackages");
// 调和阶段
reconciledPackages = ReconcilePackageUtils.reconcilePackages(
reconcileRequest, mSharedLibraries,
mPm.mSettings.getKeySetManagerService(), mPm.mSettings);
printPkg(reconciledPackages,"niulog install installPackagesLI reconcile");
} catch (ReconcileFailure e) {
for (InstallRequest request : requests) {
request.mInstallResult.setError("Reconciliation failed...", e);
}
return;
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
try {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "commitPackages");
commitRequest = new CommitRequest(reconciledPackages,
mPm.mUserManager.getUserIds()); //niu 构建CommitRequest(把前面各种阶段的信息都收集起来)
//进入commit阶段
commitPackagesLocked(commitRequest);
success = true;
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
}
} finally {
省略代码......
}
}
后期收尾
终于到了后期收尾阶段,为啥要叫后期收尾呢?是因为这一阶段所做的事情即使出现了错误也不会影响上面apk安装成功的结果,那就来看下后期收尾都做了哪些事情。
创建app data根目录
关于为什么创建app data根目录以及都创建了哪些目录可以参考installd进程,在这篇就不赘述了。创建app data根目录是委托了Installer,Installer在通过binder通信的方式让installd进程帮忙创建的。只有创建app data根目录成功后,apk才可以运行起来。
dex优化
关于dex优化可以参考installd进程,同样dex优化也是委托Installer实现的,最终也是转交由installd进程帮忙实现的。dex优化即使不成功也不会影响apk的运行,但是会影响apk的运行速度。
创建app data根目录和dex优化的源代码如下,有兴趣可以看下
//文件:InstallPackageHelper.java
private void executePostCommitSteps(CommitRequest commitRequest) {
final ArraySet<IncrementalStorage> incrementalStorages = new ArraySet<>();
for (ReconciledPackage reconciledPkg : commitRequest.mReconciledPackages.values()) {
final boolean instantApp = ((reconciledPkg.mScanResult.mRequest.mScanFlags
& SCAN_AS_INSTANT_APP) != 0);
final AndroidPackage pkg = reconciledPkg.mPkgSetting.getPkg();
final String packageName = pkg.getPackageName();
final String codePath = pkg.getPath();
final boolean onIncremental = mIncrementalManager != null
&& isIncrementalPath(codePath);
省略代码......
//创建app data根目录
mAppDataHelper.prepareAppDataPostCommitLIF(pkg, 0); //niu 创建 data目录
省略代码......
final boolean performDexopt =
(!instantApp || android.provider.Settings.Global.getInt(
mContext.getContentResolver(),
android.provider.Settings.Global.INSTANT_APP_DEXOPT_ENABLED, 0) != 0)
&& !pkg.isDebuggable()
&& (!onIncremental)
&& dexoptOptions.isCompilationEnabled();
//并不是所有的apk都需要dex优化,如果需要优化,进入下面逻辑
if (performDexopt) {
省略代码......
//开始优化
mPackageDexOptimizer.performDexOpt(pkg, realPkgSetting,
null /* instructionSets */,
mPm.getOrCreateCompilerPackageStats(pkg),
mDexManager.getPackageUseInfoOrDefault(packageName),
dexoptOptions);
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
省略代码......
}
PackageManagerServiceUtils.waitForNativeBinariesExtractionForIncremental(
incrementalStorages);
}
移除已有apk
如果设备上已经安装了相同包名的apk(称它为老apk),则在新apk安装成功后是需要把老apk删除的,删除过程也同样是委托Installer,最终转交由installd进程来实现。即使老apk删除失败也不会影响新apk。
下面是对应源码,有兴趣看下
//文件:Installer.java
public void rmPackageDir(String packageName, String packageDir) throws InstallerException {
if (!checkBeforeRemote()) return;
BlockGuard.getVmPolicy().onPathAccess(packageDir);
try {
mInstalld.rmPackageDir(packageName, packageDir);
} catch (Exception e) {
throw InstallerException.from(e);
}
}
发送安装成功广播
既然一个apk安装成功了,那肯定是需要通知关注者的,采用的方式是发广播,比如桌面在收到安装成功的广播后,修改正在安装apk的状态。
下面是发送广播源码,有兴趣看下
//文件:PackageInstallerSession.java
private void dispatchSessionFinished(int returnCode, String msg, Bundle extras) {
sendUpdateToRemoteStatusReceiver(returnCode, msg, extras);
synchronized (mLock) {
mFinalStatus = returnCode;
mFinalMessage = msg;
}
final boolean success = (returnCode == INSTALL_SUCCEEDED);
final boolean isNewInstall = extras == null || !extras.getBoolean(Intent.EXTRA_REPLACING);
if (success && isNewInstall && mPm.mInstallerService.okToSendBroadcasts()) {
//收集apk的信息,把这些信息通过广播发送出去
mPm.sendSessionCommitBroadcast(generateInfoScrubbed(true /*icon*/), userId);
}
mCallback.onSessionFinished(this, success);
if (isDataLoaderInstallation()) {
logDataLoaderInstallationSession(returnCode);
}
}
总结
到此apk的安装之谜算是揭开了,apk的安装会经过前期准备、安装、后期收尾这三个阶段,前期准备成功后才会进入安装阶段,安装阶段成功后才会进入后期收尾阶段。除了后期收尾外,前两个阶段只要发生错误就会停止apk的安装。
apk的安装可以总结为下面几步:
不管apk是通过adb安装的(apk存储于PC的磁盘)还是应用市场安装的(apk存储于设备),首先apk会被拷贝到 /data/app/xxx.tmp目录下面(xxx是一个随机生成的字符串) 在经过重重的验证、校验(签名、版本号),/data/app/xxx.tmp 目录会重命名为 /data/app/[randomStrA]/[packageName]-[randomStrB] 目录,也就是被拷贝的apk最终路径是 /data/app/[randomStrA]/[packageName]-[randomStrB]/base.apk 。同时会为apk生成一个唯一的id又称appid 解析apk的AndroidManifest中的内容为ParsedPackage,ParsedPackage中的权限等信息经过验证通过后,ParsedPackage传递给PMS,这样其他使用者比如ActivityManagerService就可以从PMS获取刚安装apk的信息了。 刚安装的apk的安装信息比如包名、版本、签名证书、安装时间等会存储到PackageSetting,PackageSetting会传递给Settings,Settings会把它持久化到packages.xml文件。 创建app data根目录,app data根目录是apk运行期间数据存储的根目录,并且app data根目录只有当前apk程序有读写执行权,其他不用没有任何权限。 对apk的dex进行优化,优化即使不成功也不影响apk的安装,dex优化可以保证app运行性能上的提升。 发送安装成功广播。
apk越大包含的so越多,安装apk的时间越长。主要时长体现在拷贝、提取native libs、dex优化这几项工作。